import datetime as dt
import time
from functools import cached_property, lru_cache, total_ordering

import pandas as pd
from dateutil import parser

from .exceptions import *
from .settings import *

pd.set_option("display.min_rows", 20)


def sleep_time_until_next_trigger(now: float, *, sec: int = 30, gap_minutes: int = 1) -> float:
    """
    set to trigger at `sec` seconds of each round(`gap_minutes`), calculate sleep time until next trigger.
    normally 30s, because of consistency under `round("T")`
    now: time.time()
    """
    assert 1 <= sec <= 59
    assert gap_minutes >= 1
    gap_seconds = gap_minutes * 60
    return int((now - sec) / gap_seconds + 1) * gap_seconds + sec - now


# '2025-01-01' utc+8
# '2025-01-01 00:00:00' utc+8
# '2025-01-09T07:28:00.000Z' utc+0
# 1735689600
# 1735689600.0
# datetime & FRDatetime
FRDate = Union[str, int, dt.datetime, "FRDatetime"]


@total_ordering
class FRDatetime:
    """
    Datetime without millisecond. Initialize with `FRDate`.
    """

    __date: dt.datetime

    def __new__(cls, date: FRDate):
        if isinstance(date, cls):
            return date
        return super().__new__(cls)

    def __init__(self, date: FRDate):
        if isinstance(date, FRDatetime):
            return
        elif isinstance(date, dt.datetime):
            if date.microsecond != 0:
                raise DatetimeError(f"cannot parse {date} with millisecond")
            self.__date = date
        elif isinstance(date, (int, float)):
            if date != int(date):
                raise DatetimeError(f"cannot parse {date} with millisecond")
            self.__date = dt.datetime.fromtimestamp(date)
        elif isinstance(date, str):
            if date.endswith("Z"):
                self.__date = parser.isoparse(date).astimezone().replace(tzinfo=None)
            else:
                if len(date) == 10:
                    self.__date = dt.datetime.strptime(date, "%Y-%m-%d")
                elif len(date) == 19:
                    if date[10] == "T":
                        self.__date = dt.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
                    else:
                        self.__date = dt.datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
                else:
                    raise DatetimeError(f"cannot parse {date} of undefined length {len(date)}")
        else:
            raise DatetimeError(f"cannot parse {date}")

    @property
    def datetime(self) -> dt.datetime:
        return self.__date

    @cached_property
    def timestamp(self) -> int:
        return int(self.__date.timestamp())

    def isoformat(self) -> str:
        """
        2025-01-01T08:00:00+08:00
        """
        return self.__date.astimezone().isoformat()

    def __repr__(self):
        return f"{self.__date}"

    def __hash__(self):
        return hash(self.__date)

    def __eq__(self, other: FRDate):
        return self.timestamp == FRDatetime(other).timestamp

    def __lt__(self, other: FRDate):
        return self.timestamp < FRDatetime(other).timestamp

    def offset(self, *, hours=0, minutes=0, seconds=0) -> "FRDatetime":
        """
        Offset with a given timedelta. Generate a new `FRDatetime`.
        """
        return FRDatetime(self.datetime + dt.timedelta(hours=hours, minutes=minutes, seconds=seconds))

    @staticmethod
    def range(start: FRDate, end: FRDate, *, hours=0, minutes=0, seconds=0) -> List["FRDatetime"]:
        """
        List of FRDatetime, [start, end), with a fixed timedelta.
        """
        start = FRDatetime(start)
        end = FRDatetime(end)
        assert start <= end, f"start {start} should be earlier than end {end}"
        tmp = []
        while start < end:
            tmp.append(start)
            start = start.offset(hours=hours, minutes=minutes, seconds=seconds)
        return tmp

    @staticmethod
    def now() -> "FRDatetime":
        return FRDatetime(int(time.time()))


def to_pd_time(t):
    return pd.to_datetime(FRDatetime(t).timestamp, unit="s", utc=True).tz_convert("Asia/Shanghai")
